/*
 * MIT License
 *
 * Copyright (c) 2021 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.seaboot.excel;

import cn.seaboot.commons.core.Asserts;
import cn.seaboot.commons.lang.Warning;
import cn.seaboot.excel.call.RowReadCallback;
import cn.seaboot.excel.call.StartupCallback;
import com.github.pjfanning.xlsx.StreamingReader;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.jetbrains.annotations.NotNull;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;

/**
 * 07 版本的 Excel 工具
 * <p>
 * 读取超大文件的写法太过另类，因此单独封装，文件读取基于迭代器实现，
 * 迭代器的指针只能向前移动，而且不能按照行号读取，与一般文件读取写法差别很大。
 * <p>
 * 注意：Excel 函数没办法在这种读取方式中进行评估计算
 *
 * <p>
 * HSSFWorkbook 03 版本，XSSFWorkbook 07 版本
 * <p>
 * {@link Workbook}的封装类，主要针对Excel导入导出业务，函数调用上会更加优雅
 *
 * @author Mr.css
 * @version 2022-02-24 10:44
 * @since 2022-12-15 删除泛型，避免产生奇怪的代码警告，使用的时候需要注意对象类型的设置
 */
public class LargerExcelReader implements Closeable {

    /**
     * workbook
     */
    private Workbook workbook;
    /**
     * sheet
     */
    private Sheet sheet;

    /**
     * Excel 行迭代器
     */
    private Iterator<Row> iterator;
    /**
     * 数据读取回调
     */
    @SuppressWarnings(Warning.RAW_TYPES)
    private RowReadCallback reader;


    /**
     * Create workbook by excel stream. this workbook can be use to deal with large excel.
     *
     * @param is is
     * @return -
     */
    public static LargerExcelReader create(InputStream is) {
        return create(is, 100, 1024);
    }

    /**
     * Create workbook by excel stream. this workbook can be use to deal with large excel.
     *
     * @param is           is
     * @param rowCacheSize number of rows
     * @param bufferSize   buffer size in bytes
     * @return -
     */
    public static LargerExcelReader create(InputStream is, int rowCacheSize, int bufferSize) {
        try {
            LargerExcelReader builder = new LargerExcelReader();
            builder.workbook = StreamingReader.builder()
                    .rowCacheSize(rowCacheSize)
                    .bufferSize(bufferSize)
                    .open(is);
            return builder;
        } catch (EncryptedDocumentException e) {
            throw new ExcelException("read excel failed!check your file!", e);
        }
    }

    /**
     * Get the Sheet object at the given index.
     *
     * @param idx of the sheet number (0-based physical & logical)
     * @return -
     */
    public LargerExcelReader sheetAt(int idx) {
        sheet = workbook.getSheetAt(idx);
        this.iterator = sheet.iterator();
        return this;
    }

    /**
     * Get sheet with the given name
     *
     * @param name of the sheet
     * @return -
     */
    public LargerExcelReader sheetAt(String name) {
        sheet = workbook.getSheet(name);
        this.iterator = sheet.iterator();
        return this;
    }

    public Workbook getWorkbook() {
        return workbook;
    }

    /**
     * 返回当前使用的{@link Sheet}，可能程序员想自己操作 Excel。
     *
     * @return Sheet
     */
    public Sheet getSheet() {
        return this.sheet;
    }

    /**
     * 设置读写回调
     *
     * @param r 读写回调
     * @return this
     */
    public <T> LargerExcelReader setRowReader(RowReadCallback<T> r) {
        this.reader = r;
        if (r instanceof StartupCallback) {
            ((StartupCallback) r).startup(this.workbook);
        }
        return this;
    }

    /**
     * 是否还有下一行
     *
     * @return 是否有下一行数据
     */
    public boolean hasNext() {
        return this.iterator.hasNext();
    }

    /**
     * 返回下一个行对象
     */
    @NotNull
    public Row next() {
        return iterator.next();
    }

    /**
     * 读取下一行数据
     *
     * @return data
     */
    @NotNull
    @SuppressWarnings(Warning.UNCHECKED)
    public <T> T nextRowData() {
        return (T) reader.readRow(iterator.next());
    }

    /**
     * 超大文件按照范围读取
     * <p>
     * 超大文件不会将整个文件读取到内存，类似于指针，需要将指针从开始位置，移动到指定位置，然后开始读取，
     * 移动指针的过程中，会有一定性能浪费，只在超大文件按照范围读取的时候使用。
     * <p>
     * 从迭代器中读取 Excel 内容，读取内容取决于代码指定的范围。
     * 会因为异常中断数据读取。
     *
     * @param <T> 这里压制了类型转换警告，需要自己注意泛型，避免产生类型转换异常
     * @return list, if endRow below of equals startLow will return a empty list
     * @throws ExcelException come from{{@link RowReadCallback#readRow(Row)}}
     */
    @NotNull
    @SuppressWarnings({Warning.RAW_TYPES, Warning.UNCHECKED})
    public <T> List<T> read() {
        List res = new ArrayList();
        while (iterator.hasNext()) {
            Row row = iterator.next();
            Object value = this.reader.readRow(row);
            if (value != null) {
                res.add(value);
            }
        }
        return res;
    }

    /**
     * 超大文件按照范围读取
     * <p>
     * 超大文件不会将整个文件读取到内存，类似于指针，需要将指针从开始位置，移动到指定位置，然后开始读取，
     * 移动指针的过程中，会有一定性能浪费，只在超大文件按照范围读取的时候使用。
     * <p>
     * 从迭代器中读取 Excel 内容，读取内容取决于代码指定的范围。
     * 会因为异常中断数据读取。
     *
     * @param <T> 这里压制了类型转换警告，需要自己注意泛型，避免产生类型转换异常
     * @param cnt 读取数据量
     * @return list, if endRow below of equals startLow will return a empty list
     * @throws ExcelException come from{{@link RowReadCallback#readRow(Row)}}
     */
    @NotNull
    @SuppressWarnings({Warning.RAW_TYPES, Warning.UNCHECKED})
    public <T> List<T> read(int cnt) {
        Asserts.state(cnt > 0, "value can not below zero: {}", cnt);
        List res = new ArrayList();
        while (iterator.hasNext()) {
            Row row = iterator.next();
            Object value = this.reader.readRow(row);
            if (value != null) {
                res.add(value);
            }
            if (res.size() >= cnt) {
                break;
            }
        }
        return res;
    }

    /**
     * 从迭代器中读取 Excel 内容，读取内容取决于代码指定的范围。
     *
     * @throws ExcelException come from{{@link RowReadCallback#readRow(Row)}}
     */
    @SuppressWarnings({Warning.UNCHECKED, Warning.RAW_TYPES})
    public void eachRow(Consumer consumer) {
        while (this.iterator.hasNext()) {
            Row row = iterator.next();
            if (row != null) {
                consumer.accept(this.reader.readRow(row));
            }
        }
    }

    /**
     * Closes this stream and releases any system resources associated
     * with it. If the stream is already closed then invoking this
     * method has no effect.
     *
     * @throws IOException f an I/O error occurs
     */
    @Override
    public void close() throws IOException {
        if (this.workbook != null) {
            this.workbook.close();
        }
    }
}
